package com.upb.webserver.common.tools.encryption;

import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.ParametersWithID;
import org.bouncycastle.crypto.signers.SM2Signer;
import org.bouncycastle.crypto.util.PrivateKeyFactory;
import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;

/**
 * 国密签名与验签
 */
public class SignUtil {

    private static final byte[] SM2_ID = {
            (byte) 0x31, (byte) 0x32, (byte) 0x33, (byte) 0x34, (byte) 0x35, (byte) 0x36, (byte) 0x37, (byte) 0x38,
            (byte) 0x31, (byte) 0x32, (byte) 0x33, (byte) 0x34, (byte) 0x35, (byte) 0x36, (byte) 0x37, (byte) 0x38
    };

    // ******************************** Sign ********************************

//    /**
//     * Sign bytes by private key file.
//     *
//     * @param privateKeyPath path of private key file.
//     * @param signDigest     bytes to sign.
//     * @return string of signature.
//     * @throws CryptoException {@link CryptoException} throws if error.
//     * @throws IOException     {@link IOException} throws if error.
//     */
//    public static String signByPKPath(String privateKeyPath, byte[] signDigest) throws Exception {
//        PrivateKey privateKey = EccKeyUtil.getPrivateKey(privateKeyPath);
//        return signByPK(privateKey, signDigest);
//    }

    /**
     * Sign string by private key of hex.
     *
     * @param privateKeyHex private of hex.
     * @param stringToSign  string to sign.
     * @return string of signature.
     * @throws InvalidKeySpecException {@link InvalidKeySpecException} throws if error.
     * @throws IOException             {@link IOException} throws if error.
     * @throws CryptoException         {@link CryptoException} throws if error.
     */
    public static String signByPKHex(String privateKeyHex, String stringToSign) throws InvalidKeySpecException, IOException, CryptoException {
        if (StringUtils.isBlank(stringToSign)) {
            return null;
        }
        return signByPKHex(privateKeyHex, stringToSign.getBytes());
    }

    /**
     * Sign bytes by private key of hex.
     *
     * @param privateKeyHex private of hex.
     * @param signDigest    bytes to sign.
     * @return string of signature.
     * @throws InvalidKeySpecException {@link InvalidKeySpecException} throws if error.
     * @throws IOException             {@link IOException} throws if error.
     * @throws CryptoException         {@link CryptoException} throws if error.
     */
    public static String signByPKHex(String privateKeyHex, byte[] signDigest) throws InvalidKeySpecException, IOException, CryptoException {
        BigInteger privateKeyBG = new BigInteger(HexUtil.hexToByteArray(privateKeyHex));
        PrivateKey privateKey = EccKeyUtil.getPrivateKey(privateKeyBG);
        return signByPK(privateKey, signDigest);
    }

    /**
     * Sign bytes by private key of {@link PrivateKey}.
     *
     * @param privateKey private of {@link PrivateKey}.
     * @param signDigest bytes to sign.
     * @return string of signature.
     * @throws IOException     {@link IOException} throws if error.
     * @throws CryptoException {@link CryptoException} throws if error.
     */
    public static String signByPK(PrivateKey privateKey, byte[] signDigest) throws IOException, CryptoException {
        byte[] signBytes = sign(signDigest, privateKey);
        //return new String(Base64.getEncoder().encode(signBytes));
        //fix: Code Correctness: Byte Array to String Conversion
        return Base64.getEncoder().encodeToString(signBytes);
    }


    /**
     * Sign bytes by private key of {@link PrivateKey}.
     *
     * @param inData     bytes to sign.
     * @param privateKey private of {@link PrivateKey}.
     * @return string of signature.
     * @throws IOException     {@link IOException} throws if error.
     * @throws CryptoException {@link CryptoException} throws if error.
     */
    private static byte[] sign(byte[] inData, PrivateKey privateKey) throws IOException, CryptoException {
        // sm3 hash
        byte[] sm3Digest = sm3Digest(inData);

        AsymmetricKeyParameter ecParam = PrivateKeyFactory.createKey(privateKey.getEncoded());
        SM2Signer sm2Signer = new SM2Signer();
        sm2Signer.init(true, new ParametersWithID(ecParam, SM2_ID));
        sm2Signer.update(sm3Digest, 0, sm3Digest.length);
        return sm2Signer.generateSignature();
    }


    // ******************************** Verify ********************************
//
//    /**
//     * Verify signature of data by public key file.
//     *
//     * @param pubKeyPath   path of private key file.
//     * @param signature    signature of data.
//     * @param originalData data to sign.
//     * @return true verify success, false when failed.
//     * @throws IOException         {@link IOException} throws if error.
//     * @throws InvalidKeyException {@link InvalidKeyException} throws if error.
//     */
//    public static boolean verifyByPubKeyPath(String pubKeyPath, String signature, String originalData)
//            throws Exception {
//        PublicKey publicKey = EccKeyUtil.getPublicKey(pubKeyPath);
//        return verifyByPubKey(publicKey, signature, originalData);
//    }

    /**
     * @return boolean
     * @Description 通过SM2公钥进行验签
     * @Param [pubKeyPath, mac, encryptionValueBytes]
     **/
    /**
     *
     * @param ecPointInHex
     * @param signature
     * @param originalData
     * @return
     * @throws InvalidKeySpecException
     * @throws InvalidKeyException
     */


    /**
     * Verify signature of data by public key of hex.
     *
     * @param ecPointInHex private key of hex.
     * @param signature    signature of data.
     * @param originalData data to sign.
     * @return true verify success, false when failed.
     * @throws IOException             {@link InvalidKeySpecException} throws if error.
     * @throws InvalidKeyException     {@link InvalidKeyException} throws if error.
     * @throws InvalidKeySpecException {@link InvalidKeySpecException} throws if error.
     */
    public static boolean verifyByEcPointHex(String ecPointInHex, String signature, String originalData)
            throws InvalidKeyException, InvalidKeySpecException {
        PublicKey publicKey = EccKeyUtil.getPublicKeyFromHex(ecPointInHex);
        return verifyByPubKey(publicKey, signature, originalData);
    }


//    /**
//     * Verify signature of data by public key of certification file.
//     *
//     * @param certPath     path of certification file.
//     * @param signature    signature of data.
//     * @param originalData data to sign.
//     * @return true verify success, false when failed.
//     * @throws CertificateException    {@link CertificateException} throws if error.
//     * @throws NoSuchProviderException {@link NoSuchProviderException} throws if error.
//     * @throws IOException             {@link IOException} throws if error.
//     * @throws InvalidKeyException     {@link InvalidKeyException} throws if error.
//     */
//    public static boolean verifyByCertPath(String certPath, String signature, String originalData)
//            throws CertificateException, NoSuchProviderException, IOException, InvalidKeyException {
//        PublicKey publicKey = EccKeyUtil.getPublicKeyFromPemCert(certPath);
//        return verifyByPubKey(publicKey, signature, originalData);
//    }

    /**
     * Verify signature of data by public key of {@link PublicKey}.
     *
     * @param publicKey    public key of {@link PublicKey}.
     * @param signature    signature of data.
     * @param originalData data to sign.
     * @return true verify success, false when failed.
     * @throws InvalidKeyException {@link InvalidKeyException} throws if error.
     */
    public static boolean verifyByPubKey(PublicKey publicKey, String signature, String originalData) throws InvalidKeyException {
        return verifySign(originalData.getBytes(), Base64.getDecoder().decode(signature), publicKey);
    }


    /**
     * Verify signature of data by public key of {@link PublicKey}.
     *
     * @param inData    data to sign.
     * @param signature signature of data.
     * @param publicKey public key of {@link PublicKey}.
     * @return true verify success, false when failed.
     * @throws InvalidKeyException {@link InvalidKeyException} throws if error.
     */
    private static boolean verifySign(byte[] inData, byte[] signature, PublicKey publicKey) throws InvalidKeyException {
        // sm3 hash
        byte[] sm3Digest = sm3Digest(inData);

        AsymmetricKeyParameter ecParam = ECUtil.generatePublicKeyParameter(publicKey);
        SM2Signer sm2Signer = new SM2Signer();
        sm2Signer.init(false, new ParametersWithID(ecParam, SM2_ID));
        sm2Signer.update(sm3Digest, 0, sm3Digest.length);
        return sm2Signer.verifySignature(signature);
    }

    /**
     * Get SM3 hash of bytes.
     *
     * @param srcBytes bytes to hash.
     * @return SM3 hash of bytes.
     */
    public static byte[] sm3Digest(byte[] srcBytes) {
        SM3Digest sm3 = new SM3Digest();
        sm3.update(srcBytes, 0, srcBytes.length);
        byte[] out = new byte[32];
        sm3.doFinal(out, 0);
        return out;
    }

//    /**
//     * Verify Base64 string is SM3 hash from src string.
//     *
//     * @param srcStr          string to hash.
//     * @param sm3Base64String hash result to verify.
//     * @return true if has result is hashed from src or else false.
//     */
//    private static boolean sm3Verify(String srcStr, String sm3Base64String) {
//        boolean flag = false;
//        byte[] sm3Hash = Base64.getDecoder().decode(sm3Base64String);
//        byte[] newHash = sm3Digest(srcStr.getBytes(StandardCharsets.UTF_8));
//        if (Arrays.equals(newHash, sm3Hash)) flag = true;
//        return flag;
//    }

    /**
     * Join elements into string.
     *
     * @param paramValueList string list to join.
     * @return joined string.
     */
    public static String buildSign(List<String> paramValueList) {
        // build param string
        return StringUtils.join(paramValueList, "");
    }
}
